Tutorial 3 - Code Refactoring, Integrating Software Tests and Error Handling
NOTE: Getting rewards from this quest is over as it was only applicable on the Earn app when it was part of a campaign. Therefore, submissions are not reviewed or are disabled. However, you are highly encouraged to still complete the quest to practice your understanding.
Let’s now focus on improving the existing code to fully use the features that Rust allows 👀
In this tutorial, you will learn how to refactor, propagate your errors and add tests on your code by solving code challenges.
ℹ️ Code challenges are what you should do as part of the quest requirements. They might include problems, puzzles, and bug fixing as challenges. Now that this is a tutorial and not part of the Earn app any more, please take your time to solve the code challenges.
💡 Keep in mind that code challenges are indicated with red AND bolded text and requirements are indicated as bullet points.
⚠️ Please read carefully the challenge requirements as missing one can lead to a rejected submission.
For technical help on the StackUp platform & quest-related questions, join our Discord, head to the 🆘 | quest-help-forum channel and look for the correct thread to ask your question.
Helpful prior knowledge
Finish the Tutorial 2 of Getting Your Hands Dirty with Rust.
Learning Outcomes
By the end of this quest, you will be able to:
- Understand the importance of software testing
- Apply better error handling through match patterns and error propagation
- Find ways to write better idiomatic Rust code
Tutorial Steps
Total steps: 7
-
Step 1: Reintroducing Rust Error-Handling
Error-handling in Rust encourages the use of the two most notable enums,
OptionandResult. Unlike Python, and other languages that “catch” errors using a try-catch pattern, Rust’s error-handling encourages that errors are propagated and are known for the lifetime of the program. This is possible in Rust becauseOptionandResultare enums.Enums are simple yet powerful
Although subjective, to communicate intent between code and programmer, enums are used to represent states in a program. A state can be anything to indicate some status e.g. if a program is running or has stopped.
RustOptionandResultare used as states to indicate- The existence or absence of a value which are handled by the
Optionenum
- The success and failure of a process which are handled by the
Resultenum
The Option enum in the Rust standard library is written as
where T is the type of the value if that value exists. Similarly, the resemblance between Option and Result enum is obvious
and we can imagine that the
Okvariant ofResultis similar to theSomevariant of Option. The only difference is thatResulthas anErrvariant that must have a type E, the value and type of the error.Handling Errors
Rust allows us to not handle errors through
unwrapfunctions and through thepanic!macro.However, it is bad practice to not handle errors especially as we might need to process the error value or even record errors to our logs. To fix the code, one might remove the panic macro and add a return type of
Resultfor the main function then lastly, add a match pattern to handle the values for success and error.Since this is a common pattern, Rust allows us to use the question mark operator. This can finally be shortened as shown below.
ℹ️ This step is useful for the following steps as there will be challenges that need at least some knowledge of error-handling in Rust.
- The existence or absence of a value which are handled by the
-
Step 2: Refactoring Account Creation
So far, our Rust project has been working fine, if we believe it does. However, you might have noticed some inconsistencies and might have realised that “this can be refactored”.
For example, this code snippet from our Rust project code seems redundant. And the annotations represent our frustrations 🤣
For example, we already have the function to initialise the database, so what’s that doing there?
You might have solutions to improve this code. Maybe even find bugs! So let’s try to improve it. The goal is to get from the old code to the new code as shown below
Adding tests
Rust has a good way to add basic tests using the
#[cfg(test)]compiler attribute and the#[test]compiler attribute. They are annotations to tell the compiler that this part of the code will only run during acargo testcommand. For example, there is already a provided code to test the Luhn algorithm. So your cargo test output will look similar as shown belowAs part of the quest challenge, add the following code anywhere in database.rs, preferably on the last line.
Then, we should configure the
database_path()function so that if we aren’t running tests, it uses a differentdatabase_path(). Copy and replace the originaldatabase_path()function with the code snippet belowℹ️ You might have noticed the #[cfg(test)] and #[cfg(not(test))]. These are compiler attributes, specifically called Configuration Conditional Checks. Like the name implies, they change your code during compile time based on the set of conditions, for example, changing what function or statement to use if we run a
cargo testor not. See more in the Rust Reference.This is required so that we can check your code is running correctly.
Challenge
In this quest’s challenge, you will have to change both database.rs and main.rs to get to the new code as shown from the previous sample screenshot. Your code should be EXACTLY similar to the sample screenshot.
The logic of the code is up to you. However, the challenge requires you to make the following code changes that are necessary to complete this quest:
- create_account should have a return type of Result<Account> in database module
- Account should have an implementation and have a new function in database module
- new function should have a return type of Result<Account> or Result<Self> in database module
- create_account should only be called inside Account::new() in database module
- Loop from old code is gone in main.rs
- Tests should pass
Always take note that the Result is from rusqlite::Result.
💡Here’s a hint: adding
AccountNumber::defaultin theAccount::newfunction as the first statement.🧠 Tips:
- Use the question mark operator (?) to propagate errors properly. See the Rust by Example section about the operator.
- Use pattern matching. They will help you a lot!
- Run
cargo clippyand let it show what you can improve. - Implement the Default trait on Account through derive to avoid the
pattern
Account { id: 0, account_number … }. - Return Result’s variants Err and Ok as much as possible if there is no other way.
- Replace unwraps by propagating the errors within closures!
🛑 Do not ever run the following cargo test command
cargo testThis is because there is a limitation of using rusqlite, specifically SQLite itself as it only works single-threaded. To run your tests on your new code, please run the following command all throughout the rest of the campaign.
cargo test -- --test-threads=1To give you more of an idea of what a test success looks like, it should be similar as shown below
Now that you have reached here, congratulate yourself for persevering throughout the campaign! You can do it! 🎉
-
Step 3: There is a bug in our code. No... Seriously
Have you noticed the bug? If you have, then congratulations 🥳
If you haven’t, then that’s fine, most of the time, bugs are harder to detect but to catch them easily, we have to catch them early. Hence, we will write some testing code 🛠️
So what’s the bug? The bug is when doing the transfers, the target account was not checked properly. Here is a screenshot of the bug below
The bug happens because we haven’t checked if the second account is valid or if it even exists.
Refactoring Again
But first, we have to refactor the code, we will remove some
eprintln!andprintln!macros and adjust our return type accordingly for the transfer.We also add the check if the target account exists so we can properly transfer. Here are the code changes for the transfer function in the database module.
New
database::transfercode. Copy this and replace the old function.ℹ️ For now, let’s ignore running the compiled binary and focus on the testing since we have removed printing information doing transfers when running the program.
Adding the tests
To verify that our code is working properly, we should start by running the command cargo test. Add this code snippet of the annotated test function transferred_balance_is_correct in the last line of the database.rs file in your tests module
☝️ However, as part of a challenge, you will have to fix the code snippet which will be discussed in the next section.
Challenge
In this step’s challenge, the requirements for this quest challenge are
- Do not change the order of the already filled statements inside transferred_balance_is_correct. Fill only below or above them indicated as comments “Fill the missing code here”.
- No code changes in other parts of the module EXCEPT transferred_balance_is_correct.
- It should return a successful test
- ALL requirements of this quest challenge must be followed
Read further for hints and tips 😏
💡 The following hint to help you finish writing the tests is adding the necessary variables to transfer between two accounts by using the
Account::new()function. The assert macros in this challenge should never be touched as they are already enough to write a test for this challenge.🧠 Tips:
- Use the question mark operator (?) to propagate errors properly. See the Rust by Example section about the operator.
- Always use the
fetch_accountfunction to update and redeclare your variables.
Once done, you can check if the
cargo testcommand returns a new and successful test -
Step 4: Quick Recap
Testing in software development is like checking your work to make sure everything runs smoothly. It's important to test important parts of your software, like complicated functions or how it connects with other programs. These tests help catch mistakes early and make sure everything works as it should, especially when you make changes or add new features.
But not everything needs a test. For example, simple things like basic functions or parts that keep changing might not need testing right away. It's also okay to skip testing if you're just trying out new ideas or working on something really old that might be tricky to test. The key is finding a balance between testing enough to catch problems and not spending too much time testing things that don't really need it.
Since our code contains a lot of newer code changes, it is up to you on how to further refactor them so that they still look like the way it runs before.
There is a lot to improve but it’s mostly up to you. For example, we can do the same to both deposit and withdraw functions by adding tests and removing the print macros. We can even create new helper functions for adding or subtracting balances of an account, thereby reusable for withdraw, deposit and transfer functions.
Please answer this survey. It helps a lot
Now that you have finally finished the quest 3 of the campaign, please answer the survey. This helps a lot for the future Rust campaigns and possible revisions of existing Rust content!
Click the link to start the survey: https://forms.gle/1Q58iJCTedY31W448
-
Step 5: Getting Your Hands Dirty with Rust! Preparing your Submission! Part 1
You have finally reached the end of the campaign! 🎉
Now, to make sure you successfully completed this quest, there is 1 deliverable that is required for this quest - one image file. Specifically, two screenshots are merged into one image (the the rest of the instructions second screenshot is on the next step).
Your first screenshot should show:
- your full screen, including your taskbar (for Windows and Linux) / dock (for MacOS)
- your code for the challenge EXCLUDING test code for it mentioned in Step 1. ALL challenge requirements should be followed
- your file explorer opened showing the files and folders of the project
- make sure that all parts visible in the expected output below are also visible in your screenshots
Expected Output for the first screenshot Proceed to the next step Part 2
-
Step 6: Getting Your Hands Dirty with Rust! Preparing your Submission! Part 2
Your second screenshot should show:
- your full screen including your taskbar (for Windows and Linux) / dock (MacOS)
- all of your test code for both challenges from Step 1 and Step 2 ALL challenge requirements should be followed
- your file explorer opened showing the files and folders of the project
- your
cargo test -- --test-threads=1output. It should show the following tests- All Luhn tests
- All database tests
- make sure that all parts visible in the expected output below are also visible in your screenshots
ℹ️ For non-VSCode or non-VSCode-fork users, you can show the file contents using your file manager and a separate terminal for the cargo run command.
Expected Output for Second screenshot Proceed to next step Part 3
-
Step 7: Getting Your Hands Dirty with Rust! Preparing your Submission! Part 3
Merge all your screenshots side-by-side. Annotate which ones are from Step 1 and which ones are from Step 2. See the example merged screenshot below
Expected Merge Screenshot Output ℹ️ If you have trouble showing all the necessary deliverables as a screenshot e.g. screen too small, you can merge more than two full screen screenshots e.g. 3 full screen screenshots (one for Account struct in Step 1, one for create_account function in Step 1, one for transferred_balance_is_correct function in mod tests in Step 2 and one for the cargo test output for Step 2). Remember to annotate them accordingly.
You can use any editing tool for that e.g. using Powerpoint to merge them on Windows, using Preview on MacOS/OSX or using ImageMagick on Linux e.g. running the following command
convert image23.png image18.png +append C26_Q3_yourusername.pngOr use a web tool such as https://www.adobe.com/express/feature/image/combine.
Refer to the images if you are unsure. If the image is too small, right click on the image and press on 'Open Image in New Tab'.
When labelling your screenshot, make sure to follow the format provided C26_Q3_yourusername.png.
Note: You can retrieve your StackUp username by clicking on the burger menu at the top right-hand corner of this page. You can check out this article for a reference on how to obtain it!
By submitting the quest, please note that our StackUp Policy prohibits the use of multiple accounts by a single user and the submission of copied work.
Find articles to support you through your journey or chat with our support team.
Help Center